package game;

import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme.app.AbstractGame;
import com.jme.app.FixedLogicrateGame;
import com.jme.input.FirstPersonHandler;
import com.jme.input.InputHandler;
import com.jme.input.KeyBindingManager;
import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.input.joystick.JoystickInput;
import com.jme.light.PointLight;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.ColorRGBA;
import com.jme.renderer.Renderer;
import com.jme.renderer.pass.BasicPassManager;
import com.jme.scene.Node;
import com.jme.scene.state.CullState;
import com.jme.scene.state.LightState;
import com.jme.scene.state.ZBufferState;
import com.jme.system.DisplaySystem;
import com.jme.system.GameSettings;
import com.jme.system.JmeException;
import com.jme.system.PropertiesGameSettings;
import com.jme.util.GameTaskQueue;
import com.jme.util.GameTaskQueueManager;
import com.jme.util.TextureManager;
import com.jme.util.Timer;
import com.jmex.audio.AudioSystem;

public abstract class GameBase extends FixedLogicrateGame {
    private static final Logger logger = Logger.getLogger(GameBase.class
            .getName());

    /**
     * The camera that we see through.
     */
    protected Camera cam;

    /**
     * The root of our scene graph.
     */
    protected Node scene;

    /**
     * The root of our hud graph.
     */
    protected Node hud;
    
    /**
     * The root of our graph.
     */
    protected Node rootNode;
    
    /**
     * Handles our mouse/keyboard input.
     */
    protected InputHandler input;

    /**
     * High resolution timer for jME.
     */
    protected Timer timer;

    /**
     * Alpha bits to use for the renderer. Any changes must be made prior to call of start().
     */
    protected int alphaBits = 0;

    /**
     * Depth bits to use for the renderer. Any changes must be made prior to call of start().
     */
    protected int depthBits = 8;

    /**
     * Stencil bits to use for the renderer. Any changes must be made prior to call of start().
     */
    protected int stencilBits = 0;

    /**
     * Number of samples to use for the multisample buffer. Any changes must be made prior to call of start().
     */
    protected int samples = 0;

    /**
     * Simply an easy way to get at timer.getTimePerFrame(). Also saves math cycles since
     * you don't call getTimePerFrame more than once per frame.
     */
    protected float tpf;

	protected LightState lightState;
	protected BasicPassManager pManager;
	
    public GameBase() {
        System.setProperty("jme.stats", "set");
    }
	
    /**
     * Updates the timer, sets tpf, updates the input and updates the fps
     * string. Also checks keys for toggling pause, bounds, normals, lights,
     * wire etc.
     *
     * @param interpolation unused in this implementation
     * @see AbstractGame#update(float interpolation)
     */
    protected void update( float interpolation ) {
        /** Recalculate the framerate. */
        timer.update();
        /** Update tpf to time per frame according to the Timer. */
        tpf = timer.getTimePerFrame();

        /** Check for key/mouse updates. */
        updateInput();
        
        // Execute updateQueue item
        GameTaskQueueManager.getManager().getQueue(GameTaskQueue.UPDATE).execute();

        /** Call simpleUpdate in any derived classes of SimpleGame. */
        simpleUpdate();

        /** Update controllers/render states/transforms/bounds for sceneNode. */
        scene.updateGeometricState(tpf, true);
        
		pManager.updatePasses(tpf);
		
        if ( KeyBindingManager.getKeyBindingManager().isValidCommand(
                "screen_shot", false ) ) {
            display.getRenderer().takeScreenShot( "SimpleGameScreenShot" );
        }

        if ( KeyBindingManager.getKeyBindingManager().isValidCommand( "exit",
                false ) ) {
            finish();
        }
        
    }

    /**
     * Check for key/mouse updates. Allow overriding this method to skip update in subclasses.
     */
    protected void updateInput() {
        input.update( tpf );
    }

    /**
     * Clears stats, the buffers and renders bounds and normals if on.
     *
     * @param interpolation unused in this implementation
     * @see AbstractGame#render(float interpolation)
     */
    protected void render( float interpolation ) {
        Renderer r = display.getRenderer();
        /** Clears the previously rendered information. */
        r.clearBuffers();
        
        // Execute renderQueue item
        GameTaskQueueManager.getManager().getQueue(GameTaskQueue.RENDER).execute();
        
        /** Draw the rootNode and all its children. */
        r.draw(scene);
        
        /** Call simpleRender() in any derived classes. */
        simpleRender();
        
        /** Have the PassManager render. */
        pManager.renderPasses(display.getRenderer());
        
        /** Draw the stats node to show our stat charts. */
        r.draw(hud);
    }

    /**
     * Creates display, sets up camera, and binds keys. Called in
     * BaseGame.start() directly after the dialog box.
     *
     * @see AbstractGame#initSystem()
     */
    protected void initSystem() throws JmeException {
        logger.info(getVersion());
        try {
            /**
             * Get a DisplaySystem acording to the renderer selected in the
             * startup box.
             */
            display = DisplaySystem.getDisplaySystem(settings.getRenderer() );
            
            display.setMinDepthBits( depthBits );
            display.setMinStencilBits( stencilBits );
            display.setMinAlphaBits( alphaBits );
            display.setMinSamples( samples );

            /** Create a window with the startup box's information. */
            display.createWindow(settings.getWidth(), settings.getHeight(),
                    settings.getDepth(), settings.getFrequency(),
                    settings.isFullscreen() );
            logger.info("Running on: " + display.getAdapter()
                    + "\nDriver version: " + display.getDriverVersion() + "\n"
                    + display.getDisplayVendor() + " - "
                    + display.getDisplayRenderer() + " - "
                    + display.getDisplayAPIVersion());
            
            
            /**
             * Create a camera specific to the DisplaySystem that works with the
             * display's width and height
             */
            cam = display.getRenderer().createCamera( display.getWidth(),
                    display.getHeight() );

        } catch ( JmeException e ) {
            /**
             * If the displaysystem can't be initialized correctly, exit
             * instantly.
             */
            logger.log(Level.SEVERE, "Could not create displaySystem", e);
            System.exit( 1 );
        }

        /** Set a black background. */
        display.getRenderer().setBackgroundColor( ColorRGBA.black.clone() );

        /** Set up how our camera sees. */
        cameraPerspective();
        Vector3f loc = new Vector3f( 0.0f, 0.0f, 25.0f );
        Vector3f left = new Vector3f( -1.0f, 0.0f, 0.0f );
        Vector3f up = new Vector3f( 0.0f, 1.0f, 0.0f );
        Vector3f dir = new Vector3f( 0.0f, 0f, -1.0f );
        /** Move our camera to a correct place and orientation. */
        cam.setFrame( loc, left, up, dir );
        /** Signal that we've changed our camera's location/frustum. */
        cam.update();
        /** Assign the camera to this renderer. */
        display.getRenderer().setCamera( cam );

        /** Create a basic input controller. */
        FirstPersonHandler firstPersonHandler = new FirstPersonHandler( cam, 50,
                1 );
        input = firstPersonHandler;

        /** Get a high resolution timer for FPS updates. */
        timer = Timer.getTimer();

        /** Sets the title of our display. */
        String className = getClass().getName();
        if ( className.lastIndexOf( '.' ) > 0 ) className = className.substring( className.lastIndexOf( '.' )+1 );
        display.setTitle( className );

        KeyBindingManager.getKeyBindingManager().set( "exit",
                KeyInput.KEY_ESCAPE );
        
        KeyBindingManager.getKeyBindingManager().set( "screen_shot",
                KeyInput.KEY_F1 );
    }

    protected void cameraPerspective() {
        cam.setFrustumPerspective( 45.0f, (float) display.getWidth()
                / (float) display.getHeight(), 1, 1000 );
        cam.setParallelProjection( false );
        cam.update();
    }

    protected void cameraParallel() {
        cam.setParallelProjection( true );
        float aspect = (float) display.getWidth() / display.getHeight();
        cam.setFrustum( -100, 1000, -50 * aspect, 50 * aspect, -50, 50 );
        cam.update();
    }

    /**
     * Creates rootNode, lighting, statistic text, and other basic render
     * states. Called in BaseGame.start() after initSystem().
     *
     * @see AbstractGame#initGame()
     */
    protected void initGame() {
    	/** Create the PassManager **/
    	pManager = new BasicPassManager();
    	
        /** Create rootNode */
        rootNode = new Node( "rootNode" );
        
        /** Create sceneNode */
        scene = new Node( "sceneNode" );
        
        /** Create hudNode */
        hud = new Node( "hudNode" );
        
        rootNode.attachChild(scene);
        rootNode.attachChild(hud);
        
        hud.setRenderQueueMode(Renderer.QUEUE_ORTHO);
        
        /**
         * Create a ZBuffer to display pixels closest to the camera above
         * farther ones.
         */
        ZBufferState buf = display.getRenderer().createZBufferState();
        buf.setEnabled( true );
        buf.setFunction( ZBufferState.TestFunction.LessThanOrEqualTo );
        scene.setRenderState( buf );
        
		// Set basic render states
		CullState cs = display.getRenderer().createCullState();
		cs.setCullFace(CullState.Face.Back);
		cs.setEnabled(true);
		scene.setRenderState(cs);
        
        scene.setRenderQueueMode(Renderer.QUEUE_OPAQUE);

        // ---- LIGHTS
        /** Set up a basic, default light. */
        PointLight light = new PointLight();
        light.setDiffuse( new ColorRGBA( 0.75f, 0.75f, 0.75f, 0.75f ) );
        light.setAmbient( new ColorRGBA( 0.5f, 0.5f, 0.5f, 1.0f ) );
        light.setLocation( new Vector3f( 100, 100, 100 ) );
        light.setEnabled( true );

        /** Attach the light to a lightState and the lightState to rootNode. */
        lightState = display.getRenderer().createLightState();
        lightState.setEnabled( true );
        lightState.attach( light );
        scene.setRenderState( lightState );

        /** Let derived classes initialize. */
        simpleInitGame();

        timer.reset();

        /**
         * Update geometric and rendering information for both the rootNode and
         * fpsNode.
         */
        scene.updateGeometricState( 0.0f, true );
        scene.updateRenderState();

        timer.reset();
    }

    /**
     * Called near end of initGame(). Must be defined by derived classes.
     */
    protected abstract void simpleInitGame();

    /**
     * Can be defined in derived classes for custom updating. Called every frame
     * in update.
     */
    protected void simpleUpdate() {
        //do nothing
    }

    /**
     * Can be defined in derived classes for custom rendering.
     * Called every frame in render.
     */
    protected void simpleRender() {
        //do nothing
    }

    /**
     * unused
     *
     * @see AbstractGame#reinit()
     */
    protected void reinit() {
        //do nothing
    }

    /**
     * Cleans up the keyboard.
     *
     * @see AbstractGame#cleanup()
     */
    protected void cleanup() {
        logger.info( "Cleaning up resources." );

        TextureManager.doTextureCleanup();
        if (display != null && display.getRenderer() != null)
            display.getRenderer().cleanup();
        KeyInput.destroyIfInitalized();
        MouseInput.destroyIfInitalized();
        JoystickInput.destroyIfInitalized();
        if (AudioSystem.isCreated()) {
            AudioSystem.getSystem().cleanup();
        }
    }

    /**
     * Calls the quit of BaseGame to clean up the display and then closes the JVM.
     */
    protected void quit() {
        super.quit();
        System.exit( 0 );
    }
    
	/**
	 * @see AbstractGame#getNewSettings()
	 */
	protected GameSettings getNewSettings() {
		return new BaseGameSettings();
	}

	/**
	 * A PropertiesGameSettings which defaults Fullscreen to TRUE.
	 */
	static class BaseGameSettings extends PropertiesGameSettings {
		static {
			// This is how you programmatically override the DEFAULT_*
			// settings of GameSettings.
			// You can also make declarative overrides by using
			// "game-defaults.properties" in a CLASSPATH root directory (or
			// use the 2-param PropertiesGameSettings constructor for any name).
			// (This is all very different from the user-specific
			// "properties.cfg"... or whatever file is specified below...,
			// which is read from the current directory and is
			// session-specific).
			defaultFullscreen = Boolean.FALSE;
			defaultSettingsWidgetImage = "/jmetest/data/images/Monkey.png";
		}

		/**
		 * Populates the GameSettings from the (session-specific) .properties
		 * file.
		 */
		BaseGameSettings() {
			super("properties.cfg");
			load();
		}
	}
}
